using System;
using System.Collections;

namespace Meta.Editor
{
	//
	// Driver for indenting and restyling text.
	// Theory of operation: The formatter is a specialization of the Reader class
	// that traps the starts and ends of subexpressions to track nesting and indentation
	// and traps beginnings of lines to update indentation.
	//
	public class Formatter : Reader {
		public static void FormatExpression(CodeBox code, int position, bool updateStyles, bool updateCommentStyles, bool showErrorPosition) {
			try {
				CodeScanner s=code.ScanCharsFrom(position);
				Formatter f = new Formatter(code, new EditorTextReader(s), updateStyles, updateCommentStyles, false);
                f.StartLine();
				f.ReindentCurrentLine();  // force indenting for first line.
				try {
					f.Read();
					// Ensure next line gets indented (to 0)
					if (!s.AtEndOfBuffer)
						s.SkipForwardToBOL();
				} catch (Exception e) {
					if (!s.AtEndOfBuffer  // Can't indent if we're off the end
						&& s.AtBeginningOfLine  // this keeps us from reindenting the first line of the next definition while allowing it to reindent lines when we hit return
						&& !s.LookingAt("[define") && !s.LookingAt("(define"))
						f.ReindentCurrentLine();
					if (showErrorPosition) {
						code.UserSelection=new Region(s.charIndex,0);
						code.RestoreUserSelection();   // Need to do this so that SelectionChanged won't call show arglist and erase our message in the arglist area
					}
					Editor ed =(Editor)code.FindForm();
					ed.SetArglistText(e.Message);
					//ed.SetValueText("Error parsing code");
				} finally {
					f.EndStyling();
				}
			} catch (Exception e) {
				Editor ed = code.FindForm() as Editor;
				if (ed!=null) {
					ed.SetArglistText("Note: "+e.Message);
				}
			}
		}

		// Setting this to true will override error handling in FormatBuffer, so you tell where the error is getting generated.
		public static bool debug = false;

		public static void FormatBuffer(CodeBox code, bool updateStyles, bool updateCommentStyles) {
			CodeScanner s=code.ScanCharsFrom(0);
			Formatter i = new Formatter(code, new EditorTextReader(s), updateStyles, updateCommentStyles, true);
			if (debug) {
				i.ReindentCurrentLine();  // force indenting for first line.
				i.SwallowWhitespaceAndComments();
				while (!i.scanner.AtEndOfBuffer) {
					i.Read();
					i.SwallowWhitespaceAndComments();
				}
				i.EndStyling();
			} else {
				try {
					i.ReindentCurrentLine();  // force indenting for first line.
					i.SwallowWhitespaceAndComments();
					while (!i.scanner.AtEndOfBuffer) {
						i.Read();
						i.SwallowWhitespaceAndComments();
					}
					i.EndStyling();
				} catch (Exception e) {
					code.UserSelection=new Region(s.charIndex,0);
					code.RestoreUserSelection();   // Need to do this so that SelectionChanged won't call show arglist and erase our message in the arglist area
					Editor ed =(Editor)code.FindForm();
					ed.SetArglistText(e.Message);
					ed.SetValueText("Error parsing code");
				}
			}
		}

		public class IndentInfo {
			public object mode=null;
			public int startIndex=0;
			public int currentIndentIndex = 0;
			public int currentIndentOffset = 0;
			public ArrayList elementIndicies = new ArrayList();
		}

		//
		// Instance variables
		//
		Stack stack = new Stack();
		CodeBox code;
		CodeScanner scanner;
		bool updateStyles;
		bool updateCommentStyles;
		int startOfStyleChunk;
		string style="Normal";
		enum TitleState {
			Idle, SkipDefine, WaitTitle
		};
		TitleState titleState = TitleState.Idle;
		bool wholeBufferMode=false;
		// Used to guess the dividing line between one top-level expression and another
		// when the first one is missing some brackets: we look for a blank line followed by an
		// optional comment and then [define.
		bool seenBlankLine=false;


		//
		// Constants (except you can change them if you want)
		//
		public static int curlyBraceOffset = 10;
		public static int bodyIndent = 2;
		public static int bodyOffset = 0;

		static System.Text.RegularExpressions.Regex checkWith = new System.Text.RegularExpressions.Regex("^[^\\[\\]\\(\\)\\{\\}=]+=");

		public Formatter(CodeBox code, EditorTextReader stream, bool updateStyles, bool updateCommentStyles, bool wholeBufferMode) : base(stream, stream.scanner.charIndex) {
			this.wholeBufferMode = wholeBufferMode;
			this.code = code;
			this.scanner = stream.scanner;
			this.updateStyles = updateStyles;
			this.updateCommentStyles = updateCommentStyles;
			startOfStyleChunk=stream.scanner.charIndex;
			style = "Normal";
			scanner.AdvancedToNewLine += new Meta.VoidThunk(StartLine);
		}

		public Formatter(CodeBox code, int position, bool updateStyles, bool updateCommentStyles)
			: this(code, new EditorTextReader(code, position), updateStyles, updateCommentStyles, false) {
		}

		public IndentInfo State {
			get {
				return (IndentInfo)(stack.Peek());
			}
		}

		// See EndElement for actual indentation logic.
		public int CurrentIndentX {
			get {
				if (stack.Count==0)
					return 0;

				IndentInfo i = State;
				int curlyBraceDeOffset = scanner.CurrentChar=='}'?(-curlyBraceOffset):0;

				return curlyBraceDeOffset+i.currentIndentOffset+code.GetPositionFromCharIndex(i.currentIndentIndex).X-code.GetPositionFromCharIndex(0).X;
			}
		}

		void StartLine() {
			if (scanner.AtBlankLine)
				seenBlankLine = true;

			if (stack.Count>0) {
				bool atDefine = scanner.LookingAt("[define") || scanner.LookingAt("(define");
				if (scanner.LookingAt(TopLevel.promptString)
					|| (code.onlyTopLevelDefs && atDefine)
					|| (!wholeBufferMode && scanner.AtBlankLine&& code.blankLinesMarkTopLevelRegions)
					|| (seenBlankLine && atDefine))
					// We've hit the next definition
					throw new SyntaxError("Expression seems to end prematurely.", null);
				if (State.mode == Symbol.sWith && !checkWith.IsMatch(scanner.lines[scanner.lineNumber]))
					BodyMode();
            }
            else if (stack.Count == 0)
            {
                if (scanner.LookingAt("scheme-formatting:"))
                {
                    code.onlyTopLevelDefs = false;
                    code.blankLinesMarkTopLevelRegions = false;
                }
                else if (scanner.LookingAt("classic-formatting:"))
                {
                    code.onlyTopLevelDefs = true;
                    code.blankLinesMarkTopLevelRegions = false;
                }
                else if (scanner.LookingAt("beginner-formatting:"))
                {
                    code.onlyTopLevelDefs = true;
                    code.blankLinesMarkTopLevelRegions = true;
                }
            }
			ReindentCurrentLine();
		}

		void ReindentCurrentLine() {
			if (stack.Count==0 && ((scanner.LookingAt("[define") || scanner.LookingAt("(define"))))
				titleState = TitleState.SkipDefine;

			code.SetRegionIndent(new Region(scanner.charIndex, 0), CurrentIndentX);
		}

		public override void StartComment() {
			NewStyleChunk(true, "Comment");
		}

		public override void EndComment() {
			NewStyleChunk(updateCommentStyles, "Normal");
		}

		public override void StartList() {
			// Set up bookkeeping information for a new nested list.
			IndentInfo n = new IndentInfo();
			n.startIndex = scanner.charIndex;
			n.currentIndentIndex = n.startIndex+1;
			n.currentIndentOffset = scanner.CurrentChar=='{'?curlyBraceOffset:0;
			stack.Push(n);
		}

		public override void EndList() {
			// Pop back to previous list
			stack.Pop();
		}

		public override void StartElement() {
			seenBlankLine = false;
			switch (titleState) {
				case TitleState.Idle:
					break;

				case TitleState.SkipDefine:
					titleState = TitleState.WaitTitle;
					break;

				case TitleState.WaitTitle:
					if (Char.GetUnicodeCategory(scanner.CurrentChar)!=System.Globalization.UnicodeCategory.OpenPunctuation) {
						NewStyleChunk(true, "Title");
						titleState = TitleState.Idle;
					} else
						// We've just seen  "[define [", so remember this should be indented as a lisp-style define body.
						BodyMode();

					break;
			}

			if (State.mode is Int32) {
				int remaining = (int)State.mode - 1;
				if (remaining>0)
					State.mode = remaining;
				else
					BodyMode();
			}

			// Remember where this element started
			State.elementIndicies.Add(scanner.charIndex);
		}

		// This does the actual indentation calculations.
		public override void EndElement(ArrayList partialList) {
			//
			// Update styles
			//
			if (style=="Title")
				NewStyleChunk(true, "Normal");

			//
			// Update indentation
			//
			IndentInfo i = State;
			Symbol op = partialList[0] as Symbol;
			int count = partialList.Count;
			object lastElement = partialList[count-1];

			if (count==1) {
				// Just got the operator
				i.currentIndentOffset = 0;   // Assume no offset unless something below sets one.
				if (op != null) {
					Parser maybeParser = Namespace.CurrentNamespace.CheckBinding(op.name) as Parser;
					if (maybeParser != null) {
						i.mode = maybeParser.IndentStyle;
					} else if (op.name.StartsWith("define") || op.name.StartsWith("with-"))
						i.mode = Symbol.sBody;
				}

				if (i.mode == Symbol.sBody && op != Symbol.sLambdaArrow)  // if op is ->, then this is an argumentless lambda, so place it under the arrow.
					BodyMode();
				else if (i.mode==null)
					// We only have one element, so indent directly underneath it.
					i.currentIndentIndex = (int)i.elementIndicies[0];
			} else if (lastElement==Symbol.sLambdaArrow || lastElement==Symbol.sAssignmentArrow) {
				// Oops; it's really a body.
				BodyMode();
			} else if ((i.mode==null || i.mode==Symbol.sWith || i.mode is Int32) && i.elementIndicies.Count==2) {
				if (lastElement == Symbol.sColon)
					BodyMode();
				else
					// This is the default case: indent to the start of the first argument
					i.currentIndentIndex = (int)i.elementIndicies[1];
			} else if (i.mode==Symbol.sWith) {
				if (count==4 && partialList[3]==Symbol.sColon)
					// For named with, as in:   with loop : a = 5
					i.currentIndentIndex = (int)i.elementIndicies[3];
			} else if (lastElement == Symbol.sAssignmentArrow && count==2) {
				// It's an assignment statement
				BodyMode();
			}
		}

		void BodyMode() {
			IndentInfo i = State;

			i.mode = Symbol.sBody;
			i.currentIndentIndex = (int)(i.elementIndicies[0])+bodyIndent;
			i.currentIndentOffset = bodyOffset;
		}

		//
		// Style updating
		//
		void NewStyleChunk(bool reformatLastChunk, string newStyle) {
			if (updateStyles && reformatLastChunk)
				code.SetRegionStyle(new Region(startOfStyleChunk, scanner.charIndex-startOfStyleChunk), code.TheStyle(style));
			style=newStyle;
			startOfStyleChunk = scanner.charIndex;
		}

		public void EndStyling() {
			if (style=="Comment")
				NewStyleChunk(updateCommentStyles, "Normal");
			else
				NewStyleChunk(true, "Normal");
		}
	}
}
